"""
文件名: auth_service.py
描述: 用户认证和授权服务

本模块包含以下主要功能：
1. Google OAuth 2.0认证流程管理
2. JWT令牌的创建、验证和刷新
3. 用户会话管理
4. 权限验证
5. 安全相关的业务逻辑

依赖模块:
   - google.auth: Google认证库
   - google_auth_oauthlib: OAuth 2.0流程
   - sqlalchemy: 数据库操作
   - redis: 会话缓存
   - typing: 类型注解

使用示例:
   >>> auth_service = AuthService()
   >>> auth_url = await auth_service.get_authorization_url("state123")
   >>> user = await auth_service.authenticate_with_code("auth_code", "state123")
   >>> token = await auth_service.create_access_token(user.id)

注意事项:
   - OAuth流程中要验证state参数防止CSRF攻击
   - 令牌要设置合理的过期时间
   - 用户敏感信息要加密存储
   - 要处理各种认证失败场景
   - 遵循OAuth 2.0安全最佳实践

作者: AI助手
创建日期: 2024-06-27
最后修改: 2024-06-27
版本: 1.0.0
"""

import secrets
from datetime import datetime, timedelta
from typing import Dict, Optional, Tuple, Any
from urllib.parse import urlencode

import httpx
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import Flow
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update
from sqlalchemy.orm import selectinload

from src.core.config import get_settings
from src.core.database import get_db_session
from src.core.redis import redis_manager
from src.core.logger import get_logger
from src.models.user import User, UserCreate, UserUpdate
from src.utils.security import (
    create_access_token,
    create_refresh_token,
    verify_token,
    encrypt_token,
    decrypt_token,
    generate_state_token,
)
from src.utils.exceptions import (
    AuthenticationException,
    InvalidTokenException,
    TokenExpiredException,
    ConfigurationException,
    ValidationException,
)

logger = get_logger(__name__)
settings = get_settings()


class AuthService:
    """认证服务类"""
    
    def __init__(self):
        """初始化认证服务"""
        self.settings = settings
        self.redis = redis_manager
        
        # OAuth 2.0配置
        self.client_config = {
            "web": {
                "client_id": settings.google_client_id,
                "client_secret": settings.google_client_secret,
                "auth_uri": "https://accounts.google.com/o/oauth2/auth",
                "token_uri": "https://oauth2.googleapis.com/token",
                "userinfo_uri": "https://www.googleapis.com/oauth2/v2/userinfo",
                "redirect_uris": [settings.google_redirect_uri],
            }
        }
        
        # OAuth作用域
        self.scopes = [
            "openid",
            "email", 
            "profile",
            "https://www.googleapis.com/auth/youtube.readonly",
        ]
        
        # 验证配置
        self._validate_config()
    
    def _validate_config(self) -> None:
        """验证OAuth配置"""
        required_configs = [
            ("google_client_id", settings.google_client_id),
            ("google_client_secret", settings.google_client_secret),
            ("google_redirect_uri", settings.google_redirect_uri),
        ]
        
        for config_name, config_value in required_configs:
            if not config_value:
                raise ConfigurationException(
                    f"缺少必需的OAuth配置: {config_name}",
                    config_key=config_name
                )
    
    async def get_authorization_url(self, state: Optional[str] = None) -> Tuple[str, str]:
        """
        获取OAuth授权URL
        
        Args:
            state: 状态参数，用于防CSRF攻击
            
        Returns:
            Tuple[str, str]: (授权URL, 状态参数)
            
        Raises:
            ConfigurationException: 配置错误时抛出
        """
        try:
            # 生成状态参数
            if not state:
                state = generate_state_token()
            
            # 创建OAuth流程
            flow = Flow.from_client_config(
                self.client_config,
                scopes=self.scopes,
                redirect_uri=settings.google_redirect_uri,
            )
            
            # 生成授权URL
            authorization_url, _ = flow.authorization_url(
                access_type="offline",  # 获取刷新令牌
                include_granted_scopes="true",  # 增量授权
                state=state,
                prompt="consent",  # 强制显示同意页面
            )
            
            # 在Redis中存储状态参数（10分钟过期）
            await self.redis.set(
                f"oauth_state:{state}",
                {"created_at": datetime.utcnow().isoformat()},
                expire=600
            )
            
            logger.info("OAuth授权URL生成成功", state=state)
            return authorization_url, state
            
        except Exception as e:
            logger.error(f"OAuth授权URL生成失败: {e}", error=str(e))
            raise AuthenticationException(f"生成授权URL失败: {str(e)}")
    
    async def authenticate_with_code(
        self,
        authorization_code: str,
        state: str,
        expected_state: Optional[str] = None,
    ) -> User:
        """
        使用授权码进行认证
        
        Args:
            authorization_code: OAuth授权码
            state: 状态参数
            expected_state: 期望的状态参数（可选）
            
        Returns:
            User: 认证成功的用户对象
            
        Raises:
            AuthenticationException: 认证失败时抛出
            ValidationException: 参数验证失败时抛出
        """
        try:
            # 验证状态参数
            await self._validate_state(state, expected_state)
            
            # 创建OAuth流程
            flow = Flow.from_client_config(
                self.client_config,
                scopes=self.scopes,
                redirect_uri=settings.google_redirect_uri,
                state=state,
            )
            
            # 使用授权码获取令牌
            flow.fetch_token(code=authorization_code)
            
            # 获取用户信息
            credentials = flow.credentials
            user_info = await self._get_user_info(credentials)
            
            # 创建或更新用户
            user = await self._create_or_update_user(user_info, credentials)
            
            # 清理状态参数
            await self.redis.delete(f"oauth_state:{state}")
            
            logger.info("OAuth认证成功", user_id=str(user.id), email=user.email)
            return user
            
        except AuthenticationException:
            raise
        except ValidationException:
            raise
        except Exception as e:
            logger.error(f"OAuth认证失败: {e}", error=str(e), code=authorization_code[:10])
            raise AuthenticationException(f"认证失败: {str(e)}")
    
    async def _validate_state(self, state: str, expected_state: Optional[str] = None) -> None:
        """
        验证OAuth状态参数
        
        Args:
            state: 收到的状态参数
            expected_state: 期望的状态参数
            
        Raises:
            ValidationException: 状态验证失败时抛出
        """
        if not state:
            raise ValidationException("缺少状态参数", field="state")
        
        if expected_state and state != expected_state:
            raise ValidationException("状态参数不匹配", field="state")
        
        # 检查Redis中的状态参数
        stored_state = await self.redis.get(f"oauth_state:{state}")
        if not stored_state:
            raise ValidationException("状态参数无效或已过期", field="state")
    
    async def _get_user_info(self, credentials: Credentials) -> Dict[str, Any]:
        """
        获取用户信息
        
        Args:
            credentials: Google OAuth凭据
            
        Returns:
            Dict[str, Any]: 用户信息
            
        Raises:
            AuthenticationException: 获取用户信息失败时抛出
        """
        try:
            async with httpx.AsyncClient() as client:
                response = await client.get(
                    "https://www.googleapis.com/oauth2/v2/userinfo",
                    headers={"Authorization": f"Bearer {credentials.token}"},
                    timeout=10.0,
                )
                response.raise_for_status()
                
                user_info = response.json()
                logger.debug("用户信息获取成功", user_id=user_info.get("id"))
                return user_info
                
        except httpx.HTTPStatusError as e:
            logger.error(f"获取用户信息HTTP错误: {e}", status_code=e.response.status_code)
            raise AuthenticationException("获取用户信息失败")
        except httpx.RequestError as e:
            logger.error(f"获取用户信息请求错误: {e}")
            raise AuthenticationException("获取用户信息失败")
        except Exception as e:
            logger.error(f"获取用户信息未知错误: {e}", error=str(e))
            raise AuthenticationException("获取用户信息失败")
    
    async def _create_or_update_user(
        self, 
        user_info: Dict[str, Any], 
        credentials: Credentials
    ) -> User:
        """
        创建或更新用户
        
        Args:
            user_info: Google用户信息
            credentials: OAuth凭据
            
        Returns:
            User: 用户对象
        """
        async with get_db_session() as db:
            try:
                google_id = user_info.get("id")
                email = user_info.get("email")
                name = user_info.get("name", "")
                avatar = user_info.get("picture", "")
                
                if not google_id or not email:
                    raise ValidationException("用户信息不完整")
                
                # 查找现有用户
                stmt = select(User).where(User.google_id == google_id)
                result = await db.execute(stmt)
                user = result.scalar_one_or_none()
                
                # 计算令牌过期时间
                token_expires_at = None
                if credentials.expiry:
                    token_expires_at = credentials.expiry
                
                if user:
                    # 更新现有用户
                    update_data = UserUpdate(
                        name=name,
                        avatar=avatar,
                        access_token=encrypt_token(credentials.token) if credentials.token else None,
                        refresh_token=encrypt_token(credentials.refresh_token) if credentials.refresh_token else None,
                        token_expires_at=token_expires_at,
                        last_login_at=datetime.utcnow(),
                        is_active=True,
                    )
                    
                    for field, value in update_data.dict(exclude_unset=True).items():
                        if value is not None:
                            setattr(user, field, value)
                    
                    logger.info("用户信息更新成功", user_id=str(user.id))
                    
                else:
                    # 创建新用户
                    user_data = UserCreate(
                        email=email,
                        name=name,
                        avatar=avatar,
                        google_id=google_id,
                        access_token=encrypt_token(credentials.token) if credentials.token else None,
                        refresh_token=encrypt_token(credentials.refresh_token) if credentials.refresh_token else None,
                        token_expires_at=token_expires_at,
                    )
                    
                    user = User(**user_data.dict())
                    user.last_login_at = datetime.utcnow()
                    
                    db.add(user)
                    await db.flush()  # 获取ID
                    
                    logger.info("新用户创建成功", user_id=str(user.id), email=email)
                
                await db.commit()
                return user
                
            except Exception as e:
                await db.rollback()
                logger.error(f"创建或更新用户失败: {e}", error=str(e))
                raise
    
    async def create_access_token(self, user_id: str) -> str:
        """
        创建访问令牌
        
        Args:
            user_id: 用户ID
            
        Returns:
            str: JWT访问令牌
        """
        try:
            token_data = {
                "sub": str(user_id),
                "type": "access_token",
            }
            
            token = create_access_token(token_data)
            
            # 在Redis中存储用户会话
            await self.redis.set(
                f"user_session:{user_id}",
                {
                    "user_id": str(user_id),
                    "created_at": datetime.utcnow().isoformat(),
                    "last_access": datetime.utcnow().isoformat(),
                },
                expire=settings.access_token_expire_minutes * 60,
            )
            
            logger.debug("访问令牌创建成功", user_id=user_id)
            return token
            
        except Exception as e:
            logger.error(f"创建访问令牌失败: {e}", error=str(e), user_id=user_id)
            raise AuthenticationException("创建访问令牌失败")
    
    async def create_refresh_token(self, user_id: str) -> str:
        """
        创建刷新令牌
        
        Args:
            user_id: 用户ID
            
        Returns:
            str: JWT刷新令牌
        """
        try:
            token_data = {
                "sub": str(user_id),
                "type": "refresh_token",
            }
            
            token = create_refresh_token(token_data)
            
            # 在Redis中存储刷新令牌（有效期7天）
            await self.redis.set(
                f"refresh_token:{user_id}",
                {
                    "token": token,
                    "created_at": datetime.utcnow().isoformat(),
                },
                expire=7 * 24 * 60 * 60,  # 7天
            )
            
            logger.debug("刷新令牌创建成功", user_id=user_id)
            return token
            
        except Exception as e:
            logger.error(f"创建刷新令牌失败: {e}", error=str(e), user_id=user_id)
            raise AuthenticationException("创建刷新令牌失败")
    
    async def verify_access_token(self, token: str) -> Optional[Dict[str, Any]]:
        """
        验证访问令牌
        
        Args:
            token: JWT访问令牌
            
        Returns:
            Optional[Dict[str, Any]]: 令牌载荷，验证失败返回None
            
        Raises:
            InvalidTokenException: 令牌无效时抛出
            TokenExpiredException: 令牌过期时抛出
        """
        try:
            payload = verify_token(token, "access_token")
            if not payload:
                raise InvalidTokenException("访问令牌无效")
            
            user_id = payload.get("sub")
            if not user_id:
                raise InvalidTokenException("令牌中缺少用户ID")
            
            # 检查用户会话是否存在
            session = await self.redis.get(f"user_session:{user_id}")
            if not session:
                raise TokenExpiredException("用户会话已过期")
            
            # 更新最后访问时间
            session["last_access"] = datetime.utcnow().isoformat()
            await self.redis.set(
                f"user_session:{user_id}",
                session,
                expire=settings.access_token_expire_minutes * 60,
            )
            
            return payload
            
        except (InvalidTokenException, TokenExpiredException):
            raise
        except Exception as e:
            logger.error(f"验证访问令牌失败: {e}", error=str(e))
            raise InvalidTokenException("访问令牌验证失败")
    
    async def refresh_access_token(self, refresh_token: str) -> Tuple[str, str]:
        """
        刷新访问令牌
        
        Args:
            refresh_token: 刷新令牌
            
        Returns:
            Tuple[str, str]: (新的访问令牌, 新的刷新令牌)
            
        Raises:
            InvalidTokenException: 刷新令牌无效时抛出
        """
        try:
            # 验证刷新令牌
            payload = verify_token(refresh_token, "refresh_token")
            if not payload:
                raise InvalidTokenException("刷新令牌无效")
            
            user_id = payload.get("sub")
            if not user_id:
                raise InvalidTokenException("刷新令牌中缺少用户ID")
            
            # 检查Redis中的刷新令牌
            stored_token = await self.redis.get(f"refresh_token:{user_id}")
            if not stored_token or stored_token.get("token") != refresh_token:
                raise InvalidTokenException("刷新令牌不匹配")
            
            # 创建新的令牌
            new_access_token = await self.create_access_token(user_id)
            new_refresh_token = await self.create_refresh_token(user_id)
            
            logger.info("令牌刷新成功", user_id=user_id)
            return new_access_token, new_refresh_token
            
        except InvalidTokenException:
            raise
        except Exception as e:
            logger.error(f"刷新令牌失败: {e}", error=str(e))
            raise InvalidTokenException("令牌刷新失败")
    
    async def logout(self, user_id: str) -> None:
        """
        用户登出
        
        Args:
            user_id: 用户ID
        """
        try:
            # 删除用户会话和刷新令牌
            await self.redis.delete(
                f"user_session:{user_id}",
                f"refresh_token:{user_id}",
            )
            
            logger.info("用户登出成功", user_id=user_id)
            
        except Exception as e:
            logger.error(f"用户登出失败: {e}", error=str(e), user_id=user_id)
            # 登出失败不抛出异常，避免影响用户体验
    
    async def get_current_user(self, token: str) -> Optional[User]:
        """
        根据令牌获取当前用户
        
        Args:
            token: 访问令牌
            
        Returns:
            Optional[User]: 用户对象，未找到返回None
        """
        try:
            payload = await self.verify_access_token(token)
            user_id = payload.get("sub")
            
            async with get_db_session() as db:
                stmt = select(User).where(
                    User.id == user_id,
                    User.is_active == True,
                    User.deleted_at.is_(None),
                )
                result = await db.execute(stmt)
                user = result.scalar_one_or_none()
                
                return user
                
        except Exception as e:
            logger.error(f"获取当前用户失败: {e}", error=str(e))
            return None


# 导出主要接口
__all__ = [
    "AuthService",
]